package cz.cuni.amis.pogamut.episodic.episodes;

import cz.cuni.amis.pogamut.episodic.memory.Affordance;
import cz.cuni.amis.pogamut.episodic.memory.IdGenerator;
import cz.cuni.amis.pogamut.episodic.memory.Parameters;
import cz.cuni.amis.pogamut.episodic.visualizer.VisualizationEvent;
import cz.cuni.amis.pogamut.episodic.visualizer.VisualizationEventType;
import java.io.Serializable;
import java.util.*;

/**
 * Chronobag represents a collection of episodes.
 *
 * @author  Zdenek Behan
 *
 * Based on the original code by Michal Cermak
 */
public class Chronobag implements Serializable {
    /**
     * Determines if a de-serialized file is compatible with this class.
     *
     * Maintainers must change this value if and only if the new version
     * of this class is not compatible with old versions. See Sun docs
     * for <a href=http://java.sun.com/products/jdk/1.1/docs/guide
     * /serialization/spec/version.doc.html> details. </a>
     *
     * Not necessary to include in first version of the class, but
     * included here as a reminder of its importance.
     */
    private static final long serialVersionUID = 1L;

    /**
     * A reference to ID generator structure, that will provide
     * the unique IDs to all inside node. This unique IDs are mostly
     * needed when identifying the visualized vertices (because nodes
     * can have identical names).
     */
    public final IdGenerator idGenerator;

    /**
     * Parent ChronoLogy structure to which this instance belongs.
     */
    public ChronoLogy chrono;

    /**
     * ID of this chronobag. Used as an ID of vertex representing
     * this chronobag when visualizing Chronobag View.
     */
    public final int id;

    /**
     * Determines whether this chronobag is old enough to pass a landmark
     * threshold. No episodes are forgotten from landmark episodes.
     */
    boolean landmark = false;

    /**
     * Specifies the level of abstraction of temporal location of chronobag's
     * episodes. Chronobag can contain episodes from single day or from longer
     * time frame (e.g. a week). Level 0 zero means the chronobag is daily
     * chronobag. Higher level means the chronobag contains episodes from
     * longer time period.
     * <p>
     * Default value is 0: daily chronobag.
     */
    int level = 0;

    /**
     * List of finished episodes. Most episodes remembered
     * will be in this list. An episode gets here from <code>currentEpisodes</code>
     * once it is finished. In past chronobags (all except
     * the current day) all episodes are stored here, because
     * all episodes are already finished.
     */
    ArrayList<Episode> finishedEpisodes = new ArrayList<Episode>();

    /**
     * List of current episodes. This is a list of episodes that
     * were not finished yet. Each time a new action is performed,
     * it has to be added to an episode in this list. Only current
     * chronobag will be using this list (it will be empty for past
     * chronobags).
     * <p>
     * An episode will be moved to <code>finishedEpisodes</code> once its
     * root node is finished, its goal disappears from short-term memory
     * or during agent's sleep.
     */
    HashMap<String, Episode> currentEpisodes = new HashMap<String, Episode>();

    /**
     * Link to the <code>episode<code> that was performed last - parent episode
     * of last performed action. Used to create the temporal sequence of
     * episodes.
     */
    private Episode lastEpisode = null;

    /**
     * Link to the first episode in this chronobag. Used to display episodes
     * in correct sequence when visualizing the chronobag.
     */
    private Episode firstEpisode = null;

    // Ending day of this ChronoBag.
    public int begin;
    // How long ago did it end.
    public int end;

    // Older chronobag of the same level.
    public Chronobag older = null;
    // Younger chronobag of the same level.
    public Chronobag younger = null;

    // Pointer to the more general chronobag in the higher level.
    public Chronobag parent = null;
    // Pointer to the children.
    public Chronobag[] children = null;

    // An indicator whether new node was added into this chronobags
    public boolean newNodeAdded = false;

    // Total number of EpisodeNode nodes in the chronobag.
    public int numberOfEpisodeNodes = 0;

    // Total number of ObjectNodes connections to the ObjectSlots in this chronobag.
    public int numberOfUsedObjects = 0;

    // Maximum number of nodes the chronobag could contain.
    // For daily chronobags it is calculated before first forgetting process
    // is triggered. For abstract chronobags it is derived from daily values
    // of lower level chronobags.
    private int maxNumberOfNodes = 0;

    // Score of a chronobag. It is used to determine forgetting.
    private double score;

    // Indicates whether this is a present chronobag.
    public boolean isPresent = false;

    // Map of ObjectNodes remembered by the agent, shared across chronobags.
    public HashMap<String, ObjectNode> objectNodes = new HashMap<String, ObjectNode>();

    /**
     * Instantiate the class by providing references to a common ID generator
     * and parent ChronoLogy structure.
     *
     * @param   idGen Reference to common <code>IdGenerator</code>.
     * @param   _chrono Reference to the <code>ChronoLogy</code> structure.
     */
    public Chronobag(IdGenerator idGen, ChronoLogy _chrono) {
        idGenerator = idGen;
        chrono = _chrono;
        id = idGenerator.getNewId();
        score = chrono.mem.parameters.MAX_NODE_SCORE;
    }

    public Chronobag(IdGenerator idGen, ChronoLogy _chrono, int _level) {
        this(idGen, _chrono);
        level = _level;
        if ((level != 0) && (level < chrono.mem.parameters.MAX_CHRONOBAG_LEVELS)) {
            children = new Chronobag[chrono.mem.parameters.CHRONOBAG_INTERVAL_LENGHTS[level]];
            for (int i=0; i < chrono.mem.parameters.CHRONOBAG_INTERVAL_LENGHTS[level]; i++) {
                children[i] = null;
            }
        }
    }

    /*public Chronobag(Chronobag from) {
        this(from.idGenerator, from.chrono, from.level);
        //parent = from.parent;
        older = from;
        from.younger = this;
    }*/

    /**
     * Returns a <code>String</code> object representing this
     * <code>Chronobag</code>'s info. It is used to provide detailed
     * information about chronobag when it is invoked from
     * the visualization structures.
     *
     * @return  a string representation of the value of this object.
     */
    @Override
    public String toString() {
        String newline = System.getProperty("line.separator");
        String s = "";
        s += id + " Age: " + begin + "-" + end + newline;
        s += "Number of Episode Nodes: " + numberOfEpisodeNodes + newline;
        return s;
    }


    /**
     * Creates a chronobag on higher level of time abstraction whose interval
     * will include whole interval of this chronobag. If such interval already
     * exists, this method only returns it (same as <code>getMoreAbstractChronobag</code>
     * method.
     *
     * @return  Returns a newly created chronobag or the one satisfying
     * requirements if it already existed.
     */
/*    public Chronobag createMoreAbstractChronobag() {
        Chronobag res = null;
        if (parent == null) {
            res = new Chronobag(idGenerator, chrono, level+1);

            int _begin = begin;
            int _end = end;
            int diff = chrono.mem.parameters.CHRONOBAG_INTERVAL_LENGHTS[level+1] - chrono.mem.parameters.CHRONOBAG_INTERVAL_LENGHTS[level];
            _begin += diff / 2;
            _end -= diff / 2;
            if (diff % 2 == 1) _begin++;
            res.begin = _begin;
            res.end = _end;
            chrono.registerNewPastChronobag(res);

            Chronobag c = this;
            parent = res;
            while (c.younger != null) {
                c = c.younger;
                c.parent = res;
            }
            c = this;
            while (c.older != null) {
                c = c.older;
                c.parent = res;
            }
            return res;
        }
        res = parent;
        Chronobag temp = res;
        while (res != null && res.begin < begin) {
            //res = res.getNextChronobag();
            temp = res;
            res = res.older;
        }
        if (res != null && res.end <= end && res.begin >= begin) {
            return res;
        }

        res = new Chronobag(idGenerator, chrono, 0);
        if (temp.begin < begin) {
            res.younger = temp;
            res.older = temp.older;
            temp.older = res;
            if (res.older != null) {
                res.older.younger = res;
            }
        } else {
            res.older = temp;
            temp.younger = res;
        }

     /*   res.previous = temp.previous;
        if (temp.previous != null) {
            temp.previous.next = res;
        }
        res.next = temp.next;
        if (temp.next != null) {
            temp.next.previous = res;
        }*/
/*
        res.level = level + 1;
        int _begin = begin;
        int _end = end;
        int diff = chrono.mem.parameters.CHRONOBAG_INTERVAL_LENGHTS[level+1] - chrono.mem.parameters.CHRONOBAG_INTERVAL_LENGHTS[level];
        _begin += diff / 2;
        _end -= diff / 2;
        if (diff % 2 == 1) _begin++;
        res.begin = _begin;
        res.end = _end;
        chrono.registerNewPastChronobag(res);

        Chronobag c = this;
        parent = chrono.getChronobagSequenceEnds().get(level + 1);
        while (c.younger != null) {
            c = c.younger;
            c.parent = chrono.getChronobagSequenceEnds().get(level + 1);
        }
        c = this;
        while (c.older != null) {
            c = c.older;
            c.parent = chrono.getChronobagSequenceEnds().get(level + 1);
        }

        res.parent = chrono.getChronobagSequenceEnds().get(res.level + 1);
        return res;
    }*/

    /**
     * Method used to <strong>increase the age</strong> of a chronobag by <strong>one day</strong>.
     * Should be called each night for all the chronobags.
     * <p>
     * There is <strong>no reversal</strong> to calling this method - age of chronobags
     * or episodes cannot be decreased.
     * <p>
     * If age passes landmark threshold, chronobag is also marked as landmark.
     */
    public void increaseDay() {
        end++;
        begin++;
        if (end >= chrono.mem.parameters.LANDMARK_AGE_THRESHOLDS[level]) {
            landmark = true;
        }
        for (Episode e : this.finishedEpisodes) {
            e.realAge += 1;
        }
    }

    /**
     * Getter method for the <code>score</code> structure.
     *
     * @return   Score of a chronobag. It is used to determine how many
     * nodes can be kept in the chronobag after the "forgetting" process.
     */
    public double getScore() {
        return score;
    }

    /**
     * This method should be called each time agent executes an atomic action.
     * It is the only way to add new <code>EpisodeNode</code>s into the <code>Episode</code>s.
     * It finds (or creates) the current <code>Episode</code> according to
     * the top level goal in trace. And invokes the <code>addNewNode</code>
     * method on that episode while passing it the same parameters.
     * <p>
     * It is also responsible for maintaining the correct episode
     * sequence within the chronobag.
     *
     * @param   atomicAction    Name of an atomic action that was executed.
     * @param   trace   List of names of decision nodes that lead to
     * the atomic action executed. The top-level goal is first in the list and
     * parent of atomic action is last item in the list.
     * @param   affordances     List of <code>Affordance</code> objects.
     * Each object represent one filled affordance slot that was
     * needed for the execution of current atomic action.
     * @param   time            Time of execution of this node.
     */
    public boolean addNewNode(String nodeName, ArrayList<String> _trace, ArrayList<Affordance> affordances, long time) throws Exception {
        ArrayList<String> trace = new ArrayList<String>(_trace); // Duplicate to make it immutable

        if (trace.size() == 0) return false;
        String topLevelGoal = trace.get(0);
        //TODO: check if it is really a top-level goal (could be spontaneous action)
        if (!currentEpisodes.containsKey(topLevelGoal)) {
            Episode episode = new Episode(this);
            if (firstEpisode == null) firstEpisode = episode;
            currentEpisodes.put(topLevelGoal, episode);
        }

        Episode current = currentEpisodes.get(topLevelGoal);

        //update episode sequence - link current and last episode if they are instances of different episodes
        if (lastEpisode == null) lastEpisode = current;
        if (current != lastEpisode) {
            lastEpisode.next.add(current);
            current.previous.add(0, lastEpisode);
        }
        lastEpisode = current;

        boolean rv = current.addNewNode(nodeName, trace, affordances, time);
        this.chrono.mem.schemas.updateSchema(nodeName, trace, affordances);
        return rv;
    }

    /** Adds new node to the map of <code>ObjectNode</code>s remembered
     * by the agent. This map is shared among the chronobags so
     * it is possible to find all usages of specified object.
     * Object are indexed by their unique name.
     * <p>
     * New node is not created if it already exists.
     *
     * @param   item    Unique name of an object.
     * @return  Returns a reference to the <code>ObjectNode</code> representing
     * the item in the parameter.
     */
    public ObjectNode createObjectNode(String item) {
        if (objectNodes.containsKey(item)) {
            return objectNodes.get(item);
        }
        objectNodes.put(item, new ObjectNode(item, idGenerator.getNewId()));
        return objectNodes.get(item);
    }

    /**
     * Marks specified episodic node as finished. If the root node of
     * an episode is marked as finished, whole episode is moved from
     * <code>currentEpisodes</code> list to <code>finishedEpisodes</code> list.
     *
     * @param   node    Name of a node that was finished.
     * @param   trace   Names of traces nodes that lead to the finished node,
     * beginning with top-level goal and ending with node's parent.
     * Should be empty if node itself is top-level goal.
     * @param   succeeded   True if node was finished successfully.
     * @return  Returns true node was successfully located and marked,
     * false otherwise.
     */
    public boolean finishNode(String node, ArrayList<String> trace, boolean succeeded) throws Exception {
        if (trace.size() == 0) return false;
        String topLevelGoal = trace.get(0);
        if (!currentEpisodes.containsKey(topLevelGoal)) return false;
        currentEpisodes.get(topLevelGoal).finishNode(node, trace, succeeded);
        if (currentEpisodes.get(topLevelGoal).finished) {
            // Update schema for the whole episode.
            Episode e = currentEpisodes.remove(topLevelGoal);
            finishedEpisodes.add(e);
            uniteEpisode(e);
            chrono.mem.schemas.updateSchema(e);
            return true;
        }
        return false;
    }

    /**
     * Creates and returns a collection of all episodes in a chronobag.
     * Collection will contain both finished and unfinished episodes.
     *
     * @return  Returns collection of all episodes in this chronobag.
     */
    public Collection<Episode> getEpisodes() {
        Collection<Episode> col = new ArrayList<Episode>();
        col.addAll(finishedEpisodes);
        col.addAll(currentEpisodes.values());
        return col;
    }

    /**
     * A getter method for the <code>firstEpisode</code> variable.
     *
     * @return  Returns a link to the first episode in this chronobag. Used to
     * display episodes in correct sequence when visualizing the chronobag.
     */
    public Episode getFirstEpisode() {
        return firstEpisode;
    }

    /**
     * Getter method for the <code>level</code> variable.
     *
     * @return Returns the level of abstraction of temporal location of chronobag's
     * episodes. Chronobag can contain episodes from single day or from longer
     * time frame (e.g. a week). Level 0 zero means the chronobag is daily
     * chronobag. Higher level means the chronobag contains episodes from
     * longer time period.
     */
    public int getLevel() {
        return level;
    }

    /**
     * Method used to compute the score of this chronobag based on its
     * current properties and node it consists. It represents overall
     * attractiveness of the chronobag.
     *
     * @return  Returns the new score value.
     */
    public double calculateChronobagScore() {
        int attractive = numberOfNodesWithAttractivity(chrono.mem.parameters.CHRONOBAG_SCORE_NODE_THRESHOLD);
        int all = numberOfEpisodeNodes;
        if (all == 0) {
            score = 0;
            return 0;
        }
        score = attractive / all * 100;

        //assert(score <= 100);
        return score;
    }

    /**
     * Creates a copy of specified episode from this chronobag and copies
     * it to the more abstract chronobag. Episode can be copied to more
     * abstract chronobag only once. Order of episodes in more abstract
     * chronobag is maintained based on episode IDs that are generated
     * in an ascending way.
     *
     * @param e Episode to be copied.
     * @return  Returns true if episode was copied successfully, returns
     * false otherwise.
     */
    public boolean copyEpisodeToAbstractChronobag(Episode e) {
        if (e.getParentChronobag() != this) return false;
        if (e.getCopied()) return false;
        Chronobag abs = parent;
        //if (abs == null) abs = createMoreAbstractChronobag();
        Episode copy = e.createCopy(abs);
        //copy.setParentChronobag(abs);
        abs.finishedEpisodes.add(copy);
        abs.newNodeAdded = true;
        abs.numberOfEpisodeNodes += copy.getRoot().numberOfSubNodes;

        Episode predecessor = null;
        Episode successor = null;
        for (Episode ep : abs.finishedEpisodes) {
            if (ep.idEpisode < e.idEpisode) {
                if (predecessor == null || predecessor.idEpisode < ep.idEpisode) {
                    predecessor = ep;
                }
            }
            if (ep.idEpisode > e.idEpisode) {
                if (successor == null || successor.idEpisode > ep.idEpisode) {
                    successor = ep;
                }
            }
        }
        if (successor != null) {
            successor.previous.remove(predecessor);
            successor.previous.add(copy);
        }
        if (predecessor != null) {
            predecessor.next.remove(successor);
            predecessor.next.add(copy);
        }
        return true;
    }

    /**
     * Getter method for the <code>landmark</code> variable.
     *
     * @return  Returns true if chronobag is old enough to pass a landmark
     * threshold. No episodes are forgotten from landmark episodes.
     */
    public boolean isLandmark() {
        return landmark;
    }

    /**
     * Moves all the episodes from current episode list to finished episodes
     * list. Called at the end the day for present chronobag.
     */
    public void finish() {
        for (Episode e : currentEpisodes.values()) {
            finishedEpisodes.add(e);
        }
        currentEpisodes.clear();
    }

    /**
     * Returns the number of episode nodes with at least given attractivity in this
     * chronobag.
     *
     * @param min Minimum node attractivity for a node to be added into the count.
     * @return  Returns sum of nodes with at least specified score.
     */
    private int numberOfNodesWithAttractivity(double min) {
        int count = 0;
        Queue<EpisodeNode> q = new LinkedList<EpisodeNode>();
        for (Episode e : getEpisodes()) {
            q.add(e.getRoot());
        }

        EpisodeNode n;
        while (!q.isEmpty()) {
            n = q.poll();
            if (n.getAssociatedNode() == null) continue;
            if (n.getAssociatedNode().getAttractivity() >= min) count++;
            for (EpisodeNode child : n.getChildrenNodes()) {
                q.add(child);
            }
        }
        return count;
    }

    /**
     * Calculates the k-th highers score in the list of scores of episode nodes.
     * K is calculated in this method. It is based on chronobag's age and score.
     *
     * @param scores    List of the scores of chronobag's episode nodes.
     * @return  Returns k-th highest node score of chronobag's episode nodes.
     */
    private double calculateKScore(List<Double> scores) {
        calculateChronobagScore();
        int k = calculateK(end, score);
        if (k >= scores.size()) return 0;
        Collections.sort(scores);
        Collections.reverse(scores);
        if (k == 0) {
            return chrono.mem.parameters.MAX_NODE_SCORE;
        }
        k--;
        return scores.get(Math.max(k,0));
    }

    /**
     * Calculates how many nodes should the chronobag have based on its age
     * and score. Number of nodes decreases with age and increases with
     * chronobag overall score.
     *
     * @param minAge    Lower bound for age of episodes in a chronobag.
     * @param chronobagScore    Overall score of chronobag based on its nodes.
     * @return  Returns K - number of episode nodes this chronobag is capable
     * to contain. If it contains more than K nodes, nodes with lowest score
     * will be deleted.
     */
    private int calculateK (int minAge, double chronobagScore) {
        double k = maxNumberOfNodes;
        if (k > chrono.mem.parameters.MAX_CHRONOBAG_NODES)
            k = chrono.mem.parameters.MAX_CHRONOBAG_NODES;

        // Time skew, make chronobags appear flowing slower depending on the level
        double age = minAge / Math.max((double)(Parameters.CHRONOBAG_INTERVAL_LENGHTS[this.level]) / 10, 1);

        // Retention power function, given by Wagenaar
        k *= chrono.mem.parameters.FORGETTING_CURVE_COEFFICIENT * Math.pow(Math.max(age,0), chrono.mem.parameters.FORGETTING_CURVE_EXPONENT[this.level]);

        k += chronobagScore / 2;
        return (int) Math.round(k);
    }

    public void decideToForgetNodes(boolean isPresent) {
        ArrayList<Double> nodeScores = new ArrayList<Double>();

        Queue<EpisodeNode> q = new LinkedList<EpisodeNode>();
        for (Episode e : getEpisodes()) {
            q.add(e.getRoot());
        }
        EpisodeNode n;
        while (!q.isEmpty()) {
            n = q.poll();
            assert n.validateNode(n) : n.getId();
            for (ObjectSlot s : n.getObjectSlots()) {
                nodeScores.addAll(s.computeScore());
            }
            // IMPORTANT: node calculation also uses slot scores, so they should be calculated first
            nodeScores.add(n.calculateScore(isPresent));
            for (EpisodeNode child : n.getChildrenNodes()) {
                q.add(child);
            }
        }

        double kScore = calculateKScore(nodeScores);

        for (Episode e : getEpisodes()) {
            q.add(e.getRoot());
            assert e.getRoot().validateNode(e.getRoot()) : e.getRoot().getId();
        }
        while (!q.isEmpty()) {
            n = q.poll();
       //     System.out.print(n.getId() + " ");
            if (n.consumed) continue;

            Collection<EpisodeNode> pred = n.getPredecessor().values();
            Collection<EpisodeNode> suc = n.getSuccessor().values();
            EpisodeNode par = n.getParent();
            Collection<EpisodeNode> ch = n.getChildrenNodes();

            assert n.validateNode(n) : n.getId();
            assert n.validateNode(par) : n.getId();
            assert n.validateNode(ch) : n.getId();
            assert n.validateNode(pred) : n.getId();
            assert n.validateNode(suc) : n.getId();

            for (EpisodeNode child : n.getChildrenNodes()) {
                if (!q.contains(child)) {
                //assert !q.contains(child);
                    q.add(child);
                }
            }
            Collection<ObjectSlot> col = new HashSet<ObjectSlot>();
            col.addAll(n.getObjectSlots());
            for (ObjectSlot s : col) {
                s.forgetConnections(kScore);
                if (s.getUsedObjects().isEmpty()) s.deleteSlot();
            }

            assert n.validateNode(par) : n.getId();
            assert n.validateNode(ch) : n.getId();
            assert n.validateNode(pred) : n.getId();
            assert n.validateNode(suc) : n.getId();

            boolean deleted = false;
            if (n.getScore() < kScore)  {
                n.deleteNode();
                deleted = true;
            }

            if (!deleted) {
                assert n.validateNode(par) : n.getId();
                assert n.validateNode(ch) : n.getId();
                assert n.validateNode(pred) : n.getId();
                assert n.validateNode(suc) : n.getId();
            }
        }
    }

    public void mergeEpisodes() {
        for (Episode e1 : getEpisodes()) {
            for (Episode e2 : getEpisodes()) {
                if (e1.getIdEpisode() < e2.getIdEpisode()) {
                    double similarity = e1.episodeSimilarity(e2);
                    double threshold = 1 - begin * chrono.mem.parameters.DECIDE_EPISODE_MERGING_COEFFICIENT;
                    // The merging threshold must never be less than 0
                    threshold = Math.max(threshold, 0);
                    if (similarity > threshold) {
                        e1.mergeWith(e2);
                    }
                }
            }
        }
    }

    void episodesMerged(Episode merged, Episode absorbed, int removedNodes, int removedNodesObjects) {
        if (currentEpisodes.containsValue(absorbed)) {
            currentEpisodes.remove(absorbed.getRoot().getName());
        }
        finishedEpisodes.remove(absorbed);
        if (absorbed == firstEpisode) firstEpisode = merged;
        if (absorbed == lastEpisode) lastEpisode = merged;
        int temp = numberOfEpisodeNodes; //because not
        deleteEpisode(absorbed);
        numberOfEpisodeNodes = temp;
        newNodeAdded = true;
        numberOfEpisodeNodes -= removedNodes;
    }

    public void reviewActiveEpisodes(ArrayList<String> list) {
        boolean updated = false;
        for (Map.Entry<String, Episode> entry : currentEpisodes.entrySet()) {
            if (!list.contains(entry.getKey())) {
                Episode e = entry.getValue();
                e.finished = true;
                updated = true;
            }
        }
        if (updated) {
            chrono.mem.fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.REFRESH_PRESENT_CHRONOBAG, chrono.mem));
        }
    }

    public boolean uniteEpisode(Episode e) {
        for (Episode prev : e.previous) {
            if (e.episodeSimilarity(prev) == 1) {
                prev.mergeWith(e);
                return true;
            }
        }
        return false;
    }

    public void deleteEpisode(Episode e) {
        //removes from finished episodes only, episodes should not be deleted unless chronobag is finished
        finishedEpisodes.remove(e);
        if (currentEpisodes.containsValue(e)) {
            System.err.println ("Deleting unfinished episode: " + e);
        }
        //numberOfEpisodeNodes -= 1 + e.getRoot().numberOfSubNodes;
        //not needed because the nodes are deleted in fullDelete at the end of this method
        //number of EpisodeNodes is updated there
        Episode e2;
        ArrayList<Episode> tempNext = new ArrayList<Episode>(e.next);
        Collections.reverse(e.previous);
        Iterator<Episode> it;// = e.next.iterator();

        if (e == firstEpisode) {
            if (!e.next.isEmpty()) {
                e.next.remove(0);
            }
        }
        for (Episode prev : e.previous) {
            for (int i = 0; i < prev.next.size();) {
                if (prev.next.get(i) == e) {
                    it = e.next.iterator();
                    if (!it.hasNext()) {
                        prev.next.remove(i);
                        //assert(prev.next.size() == i);
                        continue;
                    }
                    e2 = it.next();
                    it.remove();
                    if (e2 == prev) {
                        prev.next.remove(i);
                        continue;
                    }
                    prev.next.set(i, e2);
                    break;
                } else {
                    i++;
                }
            }
        }
        e.next = tempNext;
        if (e == lastEpisode) {
            if (!e.previous.isEmpty()) {
        //        e.previous.remove(e.previous.size() - 1);
            }
        }
        for (Episode n : e.next) {
            for (int i = n.previous.size() - 1; i >= 0;) {
                if (n.previous.get(i) == e) {
                    if (n.previous.size() < e.next.size() && e == firstEpisode) {
                        n.previous.remove(i);
                        break;
                    }
                    it = e.previous.iterator(); //already reversed
                    if (!it.hasNext()) {
                        n.previous.remove(i);
                        i--;
                     //   assert(n.previous.size() == i);
                        continue;
                    }
                    e2 = it.next();
                    it.remove();
                    if (e2 == n) {
                        n.previous.remove(i);
                        i--;
                        continue;
                    }
                    n.previous.set(i, e2);
                    break;
                } else {
                    i--;
                }
            }
        }
        if (e.getRoot() != null) e.getRoot().fullDelete(true);
    }

    public void deleteChronobag() {
        // Rewire chronobag structure
        if (younger != null) {
            younger.older = older;
        }
        if (older != null) {
            older.younger = younger;
        }
        // Delete us from the parent
        if (parent != null) {
            parent.unlinkChild(this);
        }
    }

    public void unlinkChild(Chronobag child) {
        int i = 0;
        for(Chronobag ch : children) {
            if (ch == child) {
                children[i] = null;
                break;
            }
            i++;
        }
    }

    /**
     * Increases the <code>maxNumberOfNodes</code> variable by the number of nodes
     * in a chronobag. <code>maxNumberOfNodes</code> is initially zero and it increase
     * only via this method. For daily chronobags it should be called just
     * once just before forgetting process is triggered for the first time
     * as the chronobag is no longer the present chronobag. For abstract
     * chronobag it is called once for each daily chronobag whose age belongs
     * to the age period of abstract chronobag.
     *
     * @param   c   Daily chronobag whose actual number of nodes will be added
     * to the maximum number of nodes for this chronobag.
     */
    public void increaseMaxNumberOfNodes(Chronobag c) {
        assert(c.level == 0);
        assert(c.end >= end);
        assert(c.begin <= begin);
        maxNumberOfNodes += c.numberOfEpisodeNodes + c.numberOfUsedObjects;
    }

    public Integer getLastEpisodeId() {
        if (lastEpisode == null) return null;
        return lastEpisode.getIdEpisode();
    }

    public Episode getEpisode(Integer id) {
        for (Episode e : getEpisodes()) {
            if (e.getIdEpisode() == id) return e;
        }
        return null;
    }
}
